package link.luyu.plugin.brochain.web3.observers;

import java.io.IOException;
import java.math.BigInteger;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import link.luyu.plugin.brochain.web3.Web3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.web3j.protocol.core.Response;
import org.web3j.protocol.core.filters.Callback;
import org.web3j.protocol.core.methods.response.EthBlockNumber;

public class BlockNumberObserver {

    private static final Logger log = LoggerFactory.getLogger(BlockNumberObserver.class);

    protected final Web3 web3;
    protected Callback<BigInteger> callback;

    protected ScheduledFuture<?> schedule;

    private BigInteger latestBlockNumber;

    public BlockNumberObserver(Web3 web3, Callback<BigInteger> callback) {
        this.web3 = web3;
        this.callback = callback;
    }

    public void run(ScheduledExecutorService scheduledExecutorService, long blockTime) {
        schedule =
                scheduledExecutorService.scheduleAtFixedRate(
                        () -> {
                            try {
                                this.poll();
                            } catch (Throwable e) {
                                // All exceptions must be caught, otherwise our job terminates
                                // without
                                // any notification
                                log.error("Error sending request", e);
                            }
                        },
                        0,
                        blockTime,
                        TimeUnit.MILLISECONDS);
    }

    private void poll() {
        EthBlockNumber ethBlockNumber = null;
        try {
            ethBlockNumber = web3.ethBlockNumber().send();
        } catch (IOException e) {
            throwException(e);
        }
        assert ethBlockNumber != null;
        if (ethBlockNumber.hasError()) {
            Response.Error error = ethBlockNumber.getError();
            throwException(error);
        } else {
            process(ethBlockNumber.getBlockNumber());
        }
    }

    private synchronized void process(BigInteger blockNumber) {
        if (latestBlockNumber == null) {
            latestBlockNumber = blockNumber;
            callback.onEvent(blockNumber);
        } else {
            while (latestBlockNumber.compareTo(blockNumber) < 0) {
                latestBlockNumber = latestBlockNumber.add(BigInteger.ONE);
                callback.onEvent(latestBlockNumber);
            }
        }
    }

    public void cancel() {
        schedule.cancel(false);
    }

    void throwException(Response.Error error) {
        throw new ObserverException(
                "Invalid request: " + (error == null ? "Unknown Error" : error.getMessage()));
    }

    void throwException(Throwable cause) {
        throw new ObserverException("Error sending request", cause);
    }
}
